"use strict";
/**
 * @class elFinder - file manager for web
 *
 * @author Dmitry (dio) Levashov
 **/
window.elFinder = function (node, opts) {
    this.time('load');

    var self = this,

		/**
		 * Node on which elfinder creating
		 *
		 * @type jQuery
		 **/
		node = $(node),

		/**
		 * Store node contents.
		 *
		 * @see this.destroy
		 * @type jQuery
		 **/
		prevContent = $('<div/>').append(node.contents()),

		/**
		 * Store node inline styles
		 *
		 * @see this.destroy
		 * @type String
		 **/
		prevStyle = node.attr('style'),

		/**
		 * Instance ID. Required to get/set cookie
		 *
		 * @type String
		 **/
		id = node.attr('id') || '',

		/**
		 * Events namespace
		 *
		 * @type String
		 **/
		namespace = 'elfinder-' + (id || Math.random().toString().substr(2, 7)),

		/**
		 * Mousedown event
		 *
		 * @type String
		 **/
		mousedown = 'mousedown.' + namespace,

		/**
		 * Keydown event
		 *
		 * @type String
		 **/
		keydown = 'keydown.' + namespace,

		/**
		 * Keypress event
		 *
		 * @type String
		 **/
		keypress = 'keypress.' + namespace,

		/**
		 * Is shortcuts/commands enabled
		 *
		 * @type Boolean
		 **/
		enabled = true,

		/**
		 * Store enabled value before ajax requiest
		 *
		 * @type Boolean
		 **/
		prevEnabled = true,

		/**
		 * List of build-in events which mapped into methods with same names
		 *
		 * @type Array
		 **/
		events = ['enable', 'disable', 'load', 'open', 'reload', 'select', 'add', 'remove', 'change', 'dblclick', 'getfile', 'lockfiles', 'unlockfiles', 'dragstart', 'dragstop'],

		/**
		 * Rules to validate data from backend
		 *
		 * @type Object
		 **/
		rules = {},

		/**
		 * Current working directory hash
		 *
		 * @type String
		 **/
		cwd = '',

		/**
		 * Current working directory options
		 *
		 * @type Object
		 **/
		cwdOptions = {
		    path: '',
		    url: '',
		    tmbUrl: '',
		    disabled: [],
		    separator: '/',
		    archives: [],
		    extract: [],
		    copyOverwrite: true,
		    tmb: false // old API
		},

		/**
		 * Files/dirs cache
		 *
		 * @type Object
		 **/
		files = {},

		/**
		 * Selected files hashes
		 *
		 * @type Array
		 **/
		selected = [],

		/**
		 * Events listeners
		 *
		 * @type Object
		 **/
		listeners = {},

		/**
		 * Shortcuts
		 *
		 * @type Object
		 **/
		shortcuts = {},

		/**
		 * Buffer for copied files
		 *
		 * @type Array
		 **/
		clipboard = [],

		/**
		 * Copied/cuted files hashes
		 * Prevent from remove its from cache.
		 * Required for dispaly correct files names in error messages
		 *
		 * @type Array
		 **/
		remember = [],

		/**
		 * Queue for 'open' requests
		 *
		 * @type Array
		 **/
		queue = [],

		/**
		 * Commands prototype
		 *
		 * @type Object
		 **/
		base = new self.command(self),

		/**
		 * elFinder node width
		 *
		 * @type String
		 * @default "auto"
		 **/
		width = 'auto',

		/**
		 * elFinder node height
		 *
		 * @type Number
		 * @default 400
		 **/
		height = 400,

		beeper = $(document.createElement('audio')).hide().appendTo('body')[0],

		syncInterval,

		open = function (data) {
		    if (data.init) {
		        // init - reset cache
		        files = {};
		    } else {
		        // remove only files from prev cwd
		        for (var i in files) {
		            if (files.hasOwnProperty(i)
					&& files[i].mime != 'directory'
					&& files[i].phash == cwd
					&& $.inArray(i, remember) === -1) {
		                delete files[i];
		            }
		        }
		    }

		    cwd = data.cwd.hash;
		    cache(data.files);
		    if (!files[cwd]) {
		        cache([data.cwd]);
		    }
		    self.lastDir(cwd);
		},

		/**
		 * Store info about files/dirs in "files" object.
		 *
		 * @param  Array  files
		 * @return void
		 **/
		cache = function (data) {
		    var l = data.length, f;

		    while (l--) {
		        f = data[l];
		        if (f.name && f.hash && f.mime) {
		            if (!f.phash) {
		                var name = 'volume_' + f.name,
							i18 = self.i18n(name);

		                if (name != i18) {
		                    f.i18 = i18;
		                }
		            }
		            files[f.hash] = f;
		        }
		    }
		},

		/**
		 * Exec shortcut
		 *
		 * @param  jQuery.Event  keydown/keypress event
		 * @return void
		 */
		execShortcut = function (e) {
		    var code = e.keyCode,
				ctrlKey = !!(e.ctrlKey || e.metaKey);

		    if (enabled) {

		        $.each(shortcuts, function (i, shortcut) {
		            if (shortcut.type == e.type
					&& shortcut.keyCode == code
					&& shortcut.shiftKey == e.shiftKey
					&& shortcut.ctrlKey == ctrlKey
					&& shortcut.altKey == e.altKey) {
		                e.preventDefault()
		                e.stopPropagation();
		                shortcut.callback(e, self);
		                self.debug('shortcut-exec', i + ' : ' + shortcut.description);
		            }
		        });

		        // prevent tab out of elfinder
		        if (code == 9 && !$(e.target).is(':input')) {
		            e.preventDefault();
		        }

		    }
		},
		date = new Date(),
		utc,
		i18n
    ;


    /**
	 * Protocol version
	 *
	 * @type String
	 **/
    this.api = null;

    /**
	 * elFinder use new api
	 *
	 * @type Boolean
	 **/
    this.newAPI = false;

    /**
	 * elFinder use old api
	 *
	 * @type Boolean
	 **/
    this.oldAPI = false;

    /**
	 * User os. Required to bind native shortcuts for open/rename
	 *
	 * @type String
	 **/
    this.OS = navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : navigator.userAgent.indexOf('Win') !== -1 ? 'win' : 'other';

    /**
	 * User browser UA.
	 * jQuery.browser: version deprecated: 1.3, removed: 1.9
	 *
	 * @type Object
	 **/
    this.UA = (function () {
        var webkit = !document.uniqueID && !window.opera && !window.sidebar && window.localStorage && typeof window.orientation == "undefined";
        return {
            // Browser IE <= IE 6
            ltIE6: typeof window.addEventListener == "undefined" && typeof document.documentElement.style.maxHeight == "undefined",
            // Browser IE <= IE 7
            ltIE7: typeof window.addEventListener == "undefined" && typeof document.querySelectorAll == "undefined",
            // Browser IE <= IE 8
            ltIE8: typeof window.addEventListener == "undefined" && typeof document.getElementsByClassName == "undefined",
            IE: document.uniqueID,
            Firefox: window.sidebar,
            Opera: window.opera,
            Webkit: webkit,
            Chrome: webkit && window.chrome,
            Safari: webkit && !window.chrome,
            Mobile: typeof window.orientation != "undefined"
        }
    })();

    /**
	 * Configuration options
	 *
	 * @type Object
	 **/
    this.options = $.extend(true, {}, this._options, opts || {});

    if (opts.ui) {
        this.options.ui = opts.ui;
    }

    if (opts.commands) {
        this.options.commands = opts.commands;
    }

    if (opts.uiOptions && opts.uiOptions.toolbar) {
        this.options.uiOptions.toolbar = opts.uiOptions.toolbar;
    }

    $.extend(this.options.contextmenu, opts.contextmenu);


    /**
	 * Ajax request type
	 *
	 * @type String
	 * @default "get"
	 **/
    this.requestType = /^(get|post)$/i.test(this.options.requestType) ? this.options.requestType.toLowerCase() : 'get',

    /**
	 * Any data to send across every ajax request
	 *
	 * @type Object
	 * @default {}
	 **/
	this.customData = $.isPlainObject(this.options.customData) ? this.options.customData : {};

    /**
	 * ID. Required to create unique cookie name
	 *
	 * @type String
	 **/
    this.id = id;

    /**
	 * URL to upload files
	 *
	 * @type String
	 **/
    this.uploadURL = opts.urlUpload || opts.url;

    /**
	 * Events namespace
	 *
	 * @type String
	 **/
    this.namespace = namespace;

    /**
	 * Interface language
	 *
	 * @type String
	 * @default "en"
	 **/
    this.lang = this.i18[this.options.lang] && this.i18[this.options.lang].messages ? this.options.lang : 'en';

    i18n = this.lang == 'en'
		? this.i18['en']
		: $.extend(true, {}, this.i18['en'], this.i18[this.lang]);

    /**
	 * Interface direction
	 *
	 * @type String
	 * @default "ltr"
	 **/
    this.direction = i18n.direction;

    /**
	 * i18 messages
	 *
	 * @type Object
	 **/
    this.messages = i18n.messages;

    /**
	 * Date/time format
	 *
	 * @type String
	 * @default "m.d.Y"
	 **/
    this.dateFormat = this.options.dateFormat || i18n.dateFormat;

    /**
	 * Date format like "Yesterday 10:20:12"
	 *
	 * @type String
	 * @default "{day} {time}"
	 **/
    this.fancyFormat = this.options.fancyDateFormat || i18n.fancyDateFormat;

    /**
	 * Today timestamp
	 *
	 * @type Number
	 **/
    this.today = (new Date(date.getFullYear(), date.getMonth(), date.getDate())).getTime() / 1000;

    /**
	 * Yesterday timestamp
	 *
	 * @type Number
	 **/
    this.yesterday = this.today - 86400;

    utc = this.options.UTCDate ? 'UTC' : '';

    this.getHours = 'get' + utc + 'Hours';
    this.getMinutes = 'get' + utc + 'Minutes';
    this.getSeconds = 'get' + utc + 'Seconds';
    this.getDate = 'get' + utc + 'Date';
    this.getDay = 'get' + utc + 'Day';
    this.getMonth = 'get' + utc + 'Month';
    this.getFullYear = 'get' + utc + 'FullYear';

    /**
	 * Css classes 
	 *
	 * @type String
	 **/
    this.cssClass = 'ui-helper-reset ui-helper-clearfix ui-widget ui-widget-content ui-corner-all elfinder elfinder-' + (this.direction == 'rtl' ? 'rtl' : 'ltr') + ' ' + this.options.cssClass;

    /**
	 * Method to store/fetch data
	 *
	 * @type Function
	 **/
    this.storage = (function () {
        try {
            return 'localStorage' in window && window['localStorage'] !== null ? self.localStorage : self.cookie;
        } catch (e) {
            return self.cookie;
        }
    })();

    this.viewType = this.storage('view') || this.options.defaultView || 'icons';

    this.sortType = this.storage('sortType') || this.options.sortType || 'name';

    this.sortOrder = this.storage('sortOrder') || this.options.sortOrder || 'asc';

    this.sortStickFolders = this.storage('sortStickFolders');

    if (this.sortStickFolders === null) {
        this.sortStickFolders = !!this.options.sortStickFolders;
    } else {
        this.sortStickFolders = !!this.sortStickFolders
    }

    this.sortRules = $.extend(true, {}, this._sortRules, this.options.sortsRules);

    $.each(this.sortRules, function (name, method) {
        if (typeof method != 'function') {
            delete self.sortRules[name];
        }
    });

    this.compare = $.proxy(this.compare, this);

    /**
	 * Delay in ms before open notification dialog
	 *
	 * @type Number
	 * @default 500
	 **/
    this.notifyDelay = this.options.notifyDelay > 0 ? parseInt(this.options.notifyDelay) : 500;

    /**
	 * Base draggable options
	 *
	 * @type Object
	 **/
    this.draggable = {
        appendTo: 'body',
        addClasses: true,
        delay: 30,
        revert: true,
        refreshPositions: true,
        cursor: 'move',
        cursorAt: { left: 50, top: 47 },
        drag: function (e, ui) {
            if (!ui.helper.data('locked')) {
                ui.helper.toggleClass('elfinder-drag-helper-plus', e.shiftKey || e.ctrlKey || e.metaKey);
            }
        },
        start: function (e, ui) {
            var targets = $.map(ui.helper.data('files') || [], function (h) { return h || null; }),
			cnt, h;
            cnt = targets.length;
            while (cnt--) {
                h = targets[cnt];
                if (files[h].locked) {
                    ui.helper.addClass('elfinder-drag-helper-plus').data('locked', true);
                    break;
                }
            }
        },
        stop: function () { self.trigger('focus').trigger('dragstop'); },
        helper: function (e, ui) {
            var element = this.id ? $(this) : $(this).parents('[id]:first'),
				helper = $('<div class="elfinder-drag-helper"><span class="elfinder-drag-helper-icon-plus"/></div>'),
				icon = function (mime) { return '<div class="elfinder-cwd-icon ' + self.mime2class(mime) + ' ui-corner-all"/>'; },
				hashes, l;

            self.trigger('dragstart', { target: element[0], originalEvent: e });

            hashes = element.is('.' + self.res('class', 'cwdfile'))
				? self.selected()
				: [self.navId2Hash(element.attr('id'))];

            helper.append(icon(files[hashes[0]].mime)).data('files', hashes).data('locked', false);

            if ((l = hashes.length) > 1) {
                helper.append(icon(files[hashes[l - 1]].mime) + '<span class="elfinder-drag-num">' + l + '</span>');
            }

            return helper;
        }
    };

    /**
	 * Base droppable options
	 *
	 * @type Object
	 **/
    this.droppable = {
        // greedy     : true,
        tolerance: 'pointer',
        accept: '.elfinder-cwd-file-wrapper,.elfinder-navbar-dir,.elfinder-cwd-file',
        hoverClass: this.res('class', 'adroppable'),
        drop: function (e, ui) {
            var dst = $(this),
                targets = $.map(ui.helper.data('files') || [], function (h) { return h || null }),
                result = [],
                c = 'class',
                cnt, hash, i, h;

            if (dst.is('.' + self.res(c, 'cwd'))) {
                hash = cwd;
            } else if (dst.is('.' + self.res(c, 'cwdfile'))) {
                hash = dst.attr('id');
            } else if (dst.is('.' + self.res(c, 'navdir'))) {
                hash = self.navId2Hash(dst.attr('id'));
            }

            cnt = targets.length;

            while (cnt--) {
                h = targets[cnt];
                // ignore drop into itself or in own location
                h != hash && files[h].phash != hash && result.push(h);
            }

            if (result.length) {
                ui.helper.hide();
                self.clipboard(result, !(e.ctrlKey || e.shiftKey || e.metaKey || ui.helper.data('locked')));
                self.exec('paste', hash);
                self.trigger('drop', { files: targets });

            }
        }
    };

    /**
	 * Return true if filemanager is active
	 *
	 * @return Boolean
	 **/
    this.enabled = function () {
        return node.is(':visible') && enabled;
    }

    /**
	 * Return true if filemanager is visible
	 *
	 * @return Boolean
	 **/
    this.visible = function () {
        return node.is(':visible');
    }

    /**
	 * Return root dir hash for current working directory
	 * 
	 * @return String
	 */
    this.root = function (hash) {
        var dir = files[hash || cwd], i;

        while (dir && dir.phash) {
            dir = files[dir.phash]
        }
        if (dir) {
            return dir.hash;
        }

        while (i in files && files.hasOwnProperty(i)) {
            dir = files[i]
            if (!dir.phash && !dir.mime == 'directory' && dir.read) {
                return dir.hash
            }
        }

        return '';
    }

    /**
	 * Return current working directory info
	 * 
	 * @return Object
	 */
    this.cwd = function () {
        return files[cwd] || {};
    }

    /**
	 * Return required cwd option
	 * 
	 * @param  String  option name
	 * @return mixed
	 */
    this.option = function (name) {
        return cwdOptions[name] || '';
    }

    /**
	 * Return file data from current dir or tree by it's hash
	 * 
	 * @param  String  file hash
	 * @return Object
	 */
    this.file = function (hash) {
        return files[hash];
    };

    /**
	 * Return all cached files
	 * 
	 * @return Array
	 */
    this.files = function () {
        return $.extend(true, {}, files);
    }

    /**
	 * Return list of file parents hashes include file hash
	 * 
	 * @param  String  file hash
	 * @return Array
	 */
    this.parents = function (hash) {
        var parents = [],
			dir;

        while ((dir = this.file(hash))) {
            parents.unshift(dir.hash);
            hash = dir.phash;
        }
        return parents;
    }

    this.path2array = function (hash, i18) {
        var file,
			path = [];

        while (hash && (file = files[hash]) && file.hash) {
            path.unshift(i18 && file.i18 ? file.i18 : file.name);
            hash = file.phash;
        }

        return path;
    }

    /**
	 * Return file path
	 * 
	 * @param  Object  file
	 * @return String
	 */
    this.path = function (hash, i18) {
        return files[hash] && files[hash].path
			? files[hash].path
			: this.path2array(hash, i18).join(cwdOptions.separator);
    }

    /**
	 * Return file url if set
	 * 
	 * @param  Object  file
	 * @return String
	 */
    this.url = function (hash) {
        var file = files[hash];

        if (!file || !file.read) {
            return '';
        }

        if (file.url) {
            return file.url;
        }

        if (cwdOptions.url) {
            return cwdOptions.url + $.map(this.path2array(hash), function (n) { return encodeURIComponent(n); }).slice(1).join('/')
        }

        var params = $.extend({}, this.customData, {
            cmd: 'file',
            target: file.hash
        });
        if (this.oldAPI) {
            params.cmd = 'open';
            params.current = file.phash;
        }
        return this.options.url + (this.options.url.indexOf('?') === -1 ? '?' : '&') + $.param(params, true);
    }

    /**
	 * Return thumbnail url
	 * 
	 * @param  String  file hash
	 * @return String
	 */
    this.tmb = function (hash) {
        var file = files[hash],
			url = file && file.tmb && file.tmb != 1 ? cwdOptions['tmbUrl'] + file.tmb : '';

        if (url && (this.UA.Opera || this.UA.IE)) {
            url += '?_=' + new Date().getTime();
        }
        return url;
    }

    /**
	 * Return selected files hashes
	 *
	 * @return Array
	 **/
    this.selected = function () {
        return selected.slice(0);
    }

    /**
	 * Return selected files info
	 * 
	 * @return Array
	 */
    this.selectedFiles = function () {
        return $.map(selected, function (hash) { return files[hash] ? $.extend({}, files[hash]) : null });
    };

    /**
	 * Return true if file with required name existsin required folder
	 * 
	 * @param  String  file name
	 * @param  String  parent folder hash
	 * @return Boolean
	 */
    this.fileByName = function (name, phash) {
        var hash;

        for (hash in files) {
            if (files.hasOwnProperty(hash) && files[hash].phash == phash && files[hash].name == name) {
                return files[hash];
            }
        }
    };

    /**
	 * Valid data for required command based on rules
	 * 
	 * @param  String  command name
	 * @param  Object  cammand's data
	 * @return Boolean
	 */
    this.validResponse = function (cmd, data) {
        return data.error || this.rules[this.rules[cmd] ? cmd : 'defaults'](data);
    }

    /**
	 * Proccess ajax request.
	 * Fired events :
	 * @todo
	 * @example
	 * @todo
	 * @return $.Deferred
	 */
    this.request = function (options) {
        var self = this,
			o = this.options,
			dfrd = $.Deferred(),
			// request data
			data = $.extend({}, o.customData, { mimes: o.onlyMimes }, options.data || options),
			// command name
			cmd = data.cmd,
			// call default fail callback (display error dialog) ?
			deffail = !(options.preventDefault || options.preventFail),
			// call default success callback ?
			defdone = !(options.preventDefault || options.preventDone),
			// options for notify dialog
			notify = $.extend({}, options.notify),
			// do not normalize data - return as is
			raw = !!options.raw,
			// sync files on request fail
			syncOnFail = options.syncOnFail,
			// open notify dialog timeout		
			timeout,
			// request options
			options = $.extend({
			    url: o.url,
			    async: true,
			    type: this.requestType,
			    dataType: 'json',
			    cache: false,
			    // timeout  : 100,
			    data: data
			}, options.options || {}),
			/**
			 * Default success handler. 
			 * Call default data handlers and fire event with command name.
			 *
			 * @param Object  normalized response data
			 * @return void
			 **/
			done = function (data) {
			    data.warning && self.error(data.warning);

			    cmd == 'open' && open($.extend(true, {}, data));

			    // fire some event to update cache/ui
			    data.removed && data.removed.length && self.remove(data);
			    data.added && data.added.length && self.add(data);
			    data.changed && data.changed.length && self.change(data);

			    // fire event with command name
			    self.trigger(cmd, data);

			    // force update content
			    data.sync && self.sync();
			},
			/**
			 * Request error handler. Reject dfrd with correct error message.
			 *
			 * @param jqxhr  request object
			 * @param String request status
			 * @return void
			 **/
			error = function (xhr, status) {
			    var error;

			    switch (status) {
			        case 'abort':
			            error = xhr.quiet ? '' : ['errConnect', 'errAbort'];
			            break;
			        case 'timeout':
			            error = ['errConnect', 'errTimeout'];
			            break;
			        case 'parsererror':
			            error = ['errResponse', 'errDataNotJSON'];
			            break;
			        default:
			            if (xhr.status == 403) {
			                error = ['errConnect', 'errAccess'];
			            } else if (xhr.status == 404) {
			                error = ['errConnect', 'errNotFound'];
			            } else {
			                error = 'errConnect';
			            }
			    }

			    dfrd.reject(error, xhr, status);
			},
			/**
			 * Request success handler. Valid response data and reject/resolve dfrd.
			 *
			 * @param Object  response data
			 * @param String request status
			 * @return void
			 **/
			success = function (response) {
			    if (raw) {
			        return dfrd.resolve(response);
			    }

			    if (!response) {
			        return dfrd.reject(['errResponse', 'errDataEmpty'], xhr);
			    } else if (!$.isPlainObject(response)) {
			        return dfrd.reject(['errResponse', 'errDataNotJSON'], xhr);
			    } else if (response.error) {
			        return dfrd.reject(response.error, xhr);
			    } else if (!self.validResponse(cmd, response)) {
			        return dfrd.reject('errResponse', xhr);
			    }

			    response = self.normalize(response);

			    if (!self.api) {
			        self.api = response.api || 1;
			        self.newAPI = self.api >= 2;
			        self.oldAPI = !self.newAPI;
			    }

			    if (response.options) {
			        cwdOptions = $.extend({}, cwdOptions, response.options);
			    }

			    if (response.netDrivers) {
			        self.netDrivers = response.netDrivers;
			    }

			    dfrd.resolve(response);
			    response.debug && self.debug('backend-debug', response.debug);
			},
			xhr, _xhr
        ;

        defdone && dfrd.done(done);
        dfrd.fail(function (error) {
            if (error) {
                deffail ? self.error(error) : self.debug('error', self.i18n(error));
            }
        })

        if (!cmd) {
            return dfrd.reject('errCmdReq');
        }

        if (syncOnFail) {
            dfrd.fail(function (error) {
                error && self.sync();
            });
        }

        if (notify.type && notify.cnt) {
            timeout = setTimeout(function () {
                self.notify(notify);
                dfrd.always(function () {
                    notify.cnt = -(parseInt(notify.cnt) || 0);
                    self.notify(notify);
                })
            }, self.notifyDelay)

            dfrd.always(function () {
                clearTimeout(timeout);
            });
        }

        // quiet abort not completed "open" requests
        if (cmd == 'open') {
            while ((_xhr = queue.pop())) {
                if (_xhr.state() == 'pending') {
                    _xhr.quiet = true;
                    _xhr.abort();
                }
            }
        }

        delete options.preventFail

        xhr = this.transport.send(options).fail(error).done(success);

        // this.transport.send(options)

        // add "open" xhr into queue
        if (cmd == 'open') {
            queue.unshift(xhr);
            dfrd.always(function () {
                var ndx = $.inArray(xhr, queue);

                ndx !== -1 && queue.splice(ndx, 1);
            });
        }

        return dfrd;
    };

    /**
	 * Compare current files cache with new files and return diff
	 * 
	 * @param  Array  new files
	 * @return Object
	 */
    this.diff = function (incoming) {
        var raw = {},
			added = [],
			removed = [],
			changed = [],
			isChanged = function (hash) {
			    var l = changed.length;

			    while (l--) {
			        if (changed[l].hash == hash) {
			            return true;
			        }
			    }
			};

        $.each(incoming, function (i, f) {
            raw[f.hash] = f;
        });

        // find removed
        $.each(files, function (hash, f) {
            !raw[hash] && removed.push(hash);
        });

        // compare files
        $.each(raw, function (hash, file) {
            var origin = files[hash];

            if (!origin) {
                added.push(file);
            } else {
                $.each(file, function (prop) {
                    if (file[prop] != origin[prop]) {
                        changed.push(file)
                        return false;
                    }
                });
            }
        });

        // parents of removed dirs mark as changed (required for tree correct work)
        $.each(removed, function (i, hash) {
            var file = files[hash],
				phash = file.phash;

            if (phash
			&& file.mime == 'directory'
			&& $.inArray(phash, removed) === -1
			&& raw[phash]
			&& !isChanged(phash)) {
                changed.push(raw[phash]);
            }
        });

        return {
            added: added,
            removed: removed,
            changed: changed
        };
    }

    /**
	 * Sync content
	 * 
	 * @return jQuery.Deferred
	 */
    this.sync = function () {
        var self = this,
			dfrd = $.Deferred().done(function () { self.trigger('sync'); }),
			opts1 = {
			    data: { cmd: 'open', init: 1, target: cwd, tree: this.ui.tree ? 1 : 0 },
			    preventDefault: true
			},
			opts2 = {
			    data: { cmd: 'tree', target: (cwd == this.root()) ? cwd : this.file(cwd).phash },
			    preventDefault: true
			};

        $.when(
			this.request(opts1),
			this.request(opts2)
		)
		.fail(function (error) {
		    dfrd.reject(error);
		    error && self.request({
		        data: { cmd: 'open', target: self.lastDir(''), tree: 1, init: 1 },
		        notify: { type: 'open', cnt: 1, hideCnt: true },
		        preventDefault: true
		    });
		})
		.done(function (odata, pdata) {
		    var diff = self.diff(odata.files.concat(pdata && pdata.tree ? pdata.tree : []));

		    diff.added.push(odata.cwd)
		    diff.removed.length && self.remove(diff);
		    diff.added.length && self.add(diff);
		    diff.changed.length && self.change(diff);
		    return dfrd.resolve(diff);
		});

        return dfrd;
    }

    this.upload = function (files) {
        return this.transport.upload(files, this);
    }

    /**
	 * Attach listener to events
	 * To bind to multiply events at once, separate events names by space
	 * 
	 * @param  String  event(s) name(s)
	 * @param  Object  event handler
	 * @return elFinder
	 */
    this.bind = function (event, callback) {
        var i;

        if (typeof (callback) == 'function') {
            event = ('' + event).toLowerCase().split(/\s+/);

            for (i = 0; i < event.length; i++) {
                if (listeners[event[i]] === void (0)) {
                    listeners[event[i]] = [];
                }
                listeners[event[i]].push(callback);
            }
        }
        return this;
    };

    /**
	 * Remove event listener if exists
	 *
	 * @param  String    event name
	 * @param  Function  callback
	 * @return elFinder
	 */
    this.unbind = function (event, callback) {
        var l = listeners[('' + event).toLowerCase()] || [],
			i = l.indexOf(callback);

        i > -1 && l.splice(i, 1);
        //delete callback; // need this?
        callback = null
        return this;
    };

    /**
	 * Fire event - send notification to all event listeners
	 *
	 * @param  String   event type
	 * @param  Object   data to send across event
	 * @return elFinder
	 */
    this.trigger = function (event, data) {
        var event = event.toLowerCase(),
			handlers = listeners[event] || [], i, j;

        this.debug('event-' + event, data)

        if (handlers.length) {
            event = $.Event(event);

            for (i = 0; i < handlers.length; i++) {
                // to avoid data modifications. remember about "sharing" passing arguments in js :) 
                event.data = $.extend(true, {}, data);

                try {
                    if (handlers[i](event, this) === false
					|| event.isDefaultPrevented()) {
                        this.debug('event-stoped', event.type);
                        break;
                    }
                } catch (ex) {
                    window.console && window.console.log && window.console.log(ex);
                }

            }
        }
        return this;
    }

    /**
	 * Bind keybord shortcut to keydown event
	 *
	 * @example
	 *    elfinder.shortcut({ 
	 *       pattern : 'ctrl+a', 
	 *       description : 'Select all files', 
	 *       callback : function(e) { ... }, 
	 *       keypress : true|false (bind to keypress instead of keydown) 
	 *    })
	 *
	 * @param  Object  shortcut config
	 * @return elFinder
	 */
    this.shortcut = function (s) {
        var patterns, pattern, code, i, parts;

        if (this.options.allowShortcuts && s.pattern && $.isFunction(s.callback)) {
            patterns = s.pattern.toUpperCase().split(/\s+/);

            for (i = 0; i < patterns.length; i++) {
                pattern = patterns[i]
                parts = pattern.split('+');
                code = (code = parts.pop()).length == 1
					? code > 0 ? code : code.charCodeAt(0)
					: $.ui.keyCode[code];

                if (code && !shortcuts[pattern]) {
                    shortcuts[pattern] = {
                        keyCode: code,
                        altKey: $.inArray('ALT', parts) != -1,
                        ctrlKey: $.inArray('CTRL', parts) != -1,
                        shiftKey: $.inArray('SHIFT', parts) != -1,
                        type: s.type || 'keydown',
                        callback: s.callback,
                        description: s.description,
                        pattern: pattern
                    };
                }
            }
        }
        return this;
    }

    /**
	 * Registered shortcuts
	 *
	 * @type Object
	 **/
    this.shortcuts = function () {
        var ret = [];

        $.each(shortcuts, function (i, s) {
            ret.push([s.pattern, self.i18n(s.description)]);
        });
        return ret;
    };

    /**
	 * Get/set clipboard content.
	 * Return new clipboard content.
	 *
	 * @example
	 *   this.clipboard([]) - clean clipboard
	 *   this.clipboard([{...}, {...}], true) - put 2 files in clipboard and mark it as cutted
	 * 
	 * @param  Array    new files hashes
	 * @param  Boolean  cut files?
	 * @return Array
	 */
    this.clipboard = function (hashes, cut) {
        var map = function () { return $.map(clipboard, function (f) { return f.hash }); }

        if (hashes !== void (0)) {
            clipboard.length && this.trigger('unlockfiles', { files: map() });
            remember = [];

            clipboard = $.map(hashes || [], function (hash) {
                var file = files[hash];
                if (file) {

                    remember.push(hash);

                    return {
                        hash: hash,
                        phash: file.phash,
                        name: file.name,
                        mime: file.mime,
                        read: file.read,
                        locked: file.locked,
                        cut: !!cut
                    }
                }
                return null;
            });
            this.trigger('changeclipboard', { clipboard: clipboard.slice(0, clipboard.length) });
            cut && this.trigger('lockfiles', { files: map() });
        }

        // return copy of clipboard instead of refrence
        return clipboard.slice(0, clipboard.length);
    }

    /**
	 * Return true if command enabled
	 * 
	 * @param  String  command name
	 * @return Boolean
	 */
    this.isCommandEnabled = function (name) {
        return this._commands[name] ? $.inArray(name, cwdOptions.disabled) === -1 : false;
    }

    /**
	 * Exec command and return result;
	 *
	 * @param  String         command name
	 * @param  String|Array   usualy files hashes
	 * @param  String|Array   command options
	 * @return $.Deferred
	 */
    this.exec = function (cmd, files, opts) {
        return this._commands[cmd] && this.isCommandEnabled(cmd)
			? this._commands[cmd].exec(files, opts)
			: $.Deferred().reject('No such command');
    }

    /**
	 * Create and return dialog.
	 *
	 * @param  String|DOMElement  dialog content
	 * @param  Object             dialog options
	 * @return jQuery
	 */
    this.dialog = function (content, options) {
        return $('<div/>').append(content).appendTo(node).elfinderdialog(options);
    }

    /**
	 * Return UI widget or node
	 *
	 * @param  String  ui name
	 * @return jQuery
	 */
    this.getUI = function (ui) {
        return this.ui[ui] || node;
    }

    this.command = function (name) {
        return name === void (0) ? this._commands : this._commands[name];
    }

    /**
	 * Resize elfinder node
	 * 
	 * @param  String|Number  width
	 * @param  Number         height
	 * @return void
	 */
    this.resize = function (w, h) {
        node.css('width', w).height(h).trigger('resize');
        this.trigger('resize', { width: node.width(), height: node.height() });
    }

    /**
	 * Restore elfinder node size
	 * 
	 * @return elFinder
	 */
    this.restoreSize = function () {
        this.resize(width, height);
    }

    this.show = function () {
        node.show();
        this.enable().trigger('show');
    }

    this.hide = function () {
        this.disable().trigger('hide');
        node.hide();
    }

    /**
	 * Destroy this elFinder instance
	 *
	 * @return void
	 **/
    this.destroy = function () {
        if (node && node[0].elfinder) {
            this.trigger('destroy').disable();
            listeners = {};
            shortcuts = {};
            $(document).add(node).unbind('.' + this.namespace);
            self.trigger = function () { }
            node.children().remove();
            node.append(prevContent.contents()).removeClass(this.cssClass).attr('style', prevStyle);
            node[0].elfinder = null;
            if (syncInterval) {
                clearInterval(syncInterval);
            }
        }
    }

    /*************  init stuffs  ****************/

    // check jquery ui
    if (!($.fn.selectable && $.fn.draggable && $.fn.droppable)) {
        return alert(this.i18n('errJqui'));
    }

    // check node
    if (!node.length) {
        return alert(this.i18n('errNode'));
    }
    // check connector url
    if (!this.options.url) {
        return alert(this.i18n('errURL'));
    }

    $.extend($.ui.keyCode, {
        'F1': 112,
        'F2': 113,
        'F3': 114,
        'F4': 115,
        'F5': 116,
        'F6': 117,
        'F7': 118,
        'F8': 119,
        'F9': 120
    });

    this.dragUpload = false;
    this.xhrUpload = (typeof XMLHttpRequestUpload != 'undefined' || typeof XMLHttpRequestEventTarget != 'undefined') && typeof File != 'undefined' && typeof FormData != 'undefined';

    // configure transport object
    this.transport = {}

    if (typeof (this.options.transport) == 'object') {
        this.transport = this.options.transport;
        if (typeof (this.transport.init) == 'function') {
            this.transport.init(this)
        }
    }

    if (typeof (this.transport.send) != 'function') {
        this.transport.send = function (opts) { return $.ajax(opts); }
    }

    if (this.transport.upload == 'iframe') {
        this.transport.upload = $.proxy(this.uploads.iframe, this);
    } else if (typeof (this.transport.upload) == 'function') {
        this.dragUpload = !!this.options.dragUploadAllow;
    } else if (this.xhrUpload && !!this.options.dragUploadAllow) {
        this.transport.upload = $.proxy(this.uploads.xhr, this);
        this.dragUpload = true;
    } else {
        this.transport.upload = $.proxy(this.uploads.iframe, this);
    }

    /**
	 * Alias for this.trigger('error', {error : 'message'})
	 *
	 * @param  String  error message
	 * @return elFinder
	 **/
    this.error = function () {
        var arg = arguments[0];
        return arguments.length == 1 && typeof (arg) == 'function'
			? self.bind('error', arg)
			: self.trigger('error', { error: arg });
    }

    // create bind/trigger aliases for build-in events
    $.each(['enable', 'disable', 'load', 'open', 'reload', 'select', 'add', 'remove', 'change', 'dblclick', 'getfile', 'lockfiles', 'unlockfiles', 'dragstart', 'dragstop', 'search', 'searchend', 'viewchange'], function (i, name) {
        self[name] = function () {
            var arg = arguments[0];
            return arguments.length == 1 && typeof (arg) == 'function'
				? self.bind(name, arg)
				: self.trigger(name, $.isPlainObject(arg) ? arg : {});
        }
    });

    // bind core event handlers
    this
		.enable(function () {
		    if (!enabled && self.visible() && self.ui.overlay.is(':hidden')) {
		        enabled = true;
		        $('texarea:focus,input:focus,button').blur();
		        node.removeClass('elfinder-disabled');
		    }
		})
		.disable(function () {
		    prevEnabled = enabled;
		    enabled = false;
		    node.addClass('elfinder-disabled');
		})
		.open(function () {
		    selected = [];
		})
		.select(function (e) {
		    selected = $.map(e.data.selected || e.data.value || [], function (hash) { return files[hash] ? hash : null; });
		})
		.error(function (e) {
		    var opts = {
		        cssClass: 'elfinder-dialog-error',
		        title: self.i18n(self.i18n('error')),
		        resizable: false,
		        destroyOnClose: true,
		        buttons: {}
		    };

		    opts.buttons[self.i18n(self.i18n('btnClose'))] = function () { $(this).elfinderdialog('close'); };

		    self.dialog('<span class="elfinder-dialog-icon elfinder-dialog-icon-error"/>' + self.i18n(e.data.error), opts);
		})
		.bind('tree parents', function (e) {
		    cache(e.data.tree || []);
		})
		.bind('tmb', function (e) {
		    $.each(e.data.images || [], function (hash, tmb) {
		        if (files[hash]) {
		            files[hash].tmb = tmb;
		        }
		    })
		})
		.add(function (e) {
		    cache(e.data.added || []);
		})
		.change(function (e) {
		    $.each(e.data.changed || [], function (i, file) {
		        var hash = file.hash;
		        if ((files[hash].width && !file.width) || (files[hash].height && !file.height)) {
		            files[hash].width = undefined;
		            files[hash].height = undefined;
		        }
		        files[hash] = files[hash] ? $.extend(files[hash], file) : file;
		    });
		})
		.remove(function (e) {
		    var removed = e.data.removed || [],
				l = removed.length,
				rm = function (hash) {
				    var file = files[hash];
				    if (file) {
				        if (file.mime == 'directory' && file.dirs) {
				            $.each(files, function (h, f) {
				                f.phash == hash && rm(h);
				            });
				        }
				        delete files[hash];
				    }
				};

		    while (l--) {
		        rm(removed[l]);
		    }

		})
		.bind('search', function (e) {
		    cache(e.data.files);
		})
		.bind('rm', function (e) {
		    var play = beeper.canPlayType && beeper.canPlayType('audio/wav; codecs="1"');

		    play && play != '' && play != 'no' && $(beeper).html('<source src="./sounds/rm.wav" type="audio/wav">')[0].play()
		})

    ;

    // bind external event handlers
    $.each(this.options.handlers, function (event, callback) {
        self.bind(event, callback);
    });

    /**
	 * History object. Store visited folders
	 *
	 * @type Object
	 **/
    this.history = new this.history(this);

    // in getFileCallback set - change default actions on double click/enter/ctrl+enter
    if (typeof (this.options.getFileCallback) == 'function' && this.commands.getfile) {
        this.bind('dblclick', function (e) {
            e.preventDefault();
            self.exec('getfile').fail(function () {
                self.exec('open');
            });
        });
        this.shortcut({
            pattern: 'enter',
            description: this.i18n('cmdgetfile'),
            callback: function () { self.exec('getfile').fail(function () { self.exec(self.OS == 'mac' ? 'rename' : 'open') }) }
        })
		.shortcut({
		    pattern: 'ctrl+enter',
		    description: this.i18n(this.OS == 'mac' ? 'cmdrename' : 'cmdopen'),
		    callback: function () { self.exec(self.OS == 'mac' ? 'rename' : 'open') }
		});

    }

    /**
	 * Loaded commands
	 *
	 * @type Object
	 **/
    this._commands = {};

    if (!$.isArray(this.options.commands)) {
        this.options.commands = [];
    }
    // check required commands
    $.each(['open', 'reload', 'back', 'forward', 'up', 'home', 'info', 'quicklook', 'getfile', 'help'], function (i, cmd) {
        $.inArray(cmd, self.options.commands) === -1 && self.options.commands.push(cmd);
    });

    // load commands
    $.each(this.options.commands, function (i, name) {
        var cmd = self.commands[name];
        if ($.isFunction(cmd) && !self._commands[name]) {
            cmd.prototype = base;
            self._commands[name] = new cmd();
            self._commands[name].setup(name, self.options.commandsOptions[name] || {});
        }
    });

    // prepare node
    node.addClass(this.cssClass)
		.bind(mousedown, function () {
		    !enabled && self.enable();
		});

    /**
	 * UI nodes
	 *
	 * @type Object
	 **/
    this.ui = {
        // container for nav panel and current folder container
        workzone: $('<div/>').appendTo(node).elfinderworkzone(this),
        // container for folders tree / places
        navbar: $('<div/>').appendTo(node).elfindernavbar(this, this.options.uiOptions.navbar || {}),
        // contextmenu
        contextmenu: $('<div/>').appendTo(node).elfindercontextmenu(this),
        // overlay
        overlay: $('<div/>').appendTo(node).elfinderoverlay({
            show: function () { self.disable(); },
            hide: function () { prevEnabled && self.enable(); }
        }),
        // current folder container
        cwd: $('<div/>').appendTo(node).elfindercwd(this, this.options.uiOptions.cwd || {}),
        // notification dialog window
        notify: this.dialog('', {
            cssClass: 'elfinder-dialog-notify',
            position: { top: '12px', right: '12px' },
            resizable: false,
            autoOpen: false,
            title: '&nbsp;',
            width: 280
        }),
        statusbar: $('<div class="ui-widget-header ui-helper-clearfix ui-corner-bottom elfinder-statusbar"/>').hide().appendTo(node)
    }

    // load required ui
    $.each(this.options.ui || [], function (i, ui) {
        var name = 'elfinder' + ui,
			opts = self.options.uiOptions[ui] || {};

        if (!self.ui[ui] && $.fn[name]) {
            self.ui[ui] = $('<' + (opts.tag || 'div') + '/>').appendTo(node)[name](self, opts);
        }
    });



    // store instance in node
    node[0].elfinder = this;

    // make node resizable
    this.options.resizable
	&& $.fn.resizable
	&& node.resizable({
	    handles: 'se',
	    minWidth: 300,
	    minHeight: 200
	});

    if (this.options.width) {
        width = this.options.width;
    }

    if (this.options.height) {
        height = parseInt(this.options.height);
    }

    // update size	
    self.resize(width, height);

    // attach events to document
    $(document)
		// disable elfinder on click outside elfinder
		.bind('click.' + this.namespace, function (e) { enabled && !$(e.target).closest(node).length && self.disable(); })
		// exec shortcuts
		.bind(keydown + ' ' + keypress, execShortcut);

    // send initial request and start to pray >_<
    this.trigger('init')
		.request({
		    data: { cmd: 'open', target: self.lastDir(), init: 1, tree: this.ui.tree ? 1 : 0 },
		    preventDone: true,
		    notify: { type: 'open', cnt: 1, hideCnt: true },
		    freeze: true
		})
		.fail(function () {
		    self.trigger('fail').disable().lastDir('');
		    listeners = {};
		    shortcuts = {};
		    $(document).add(node).unbind('.' + this.namespace);
		    self.trigger = function () { };
		})
		.done(function (data) {
		    self.load().debug('api', self.api);
		    data = $.extend(true, {}, data);
		    open(data);
		    self.trigger('open', data);
		});

    // update ui's size after init
    this.one('load', function () {
        node.trigger('resize');
        if (self.options.sync > 1000) {
            syncInterval = setInterval(function () {
                self.sync();
            }, self.options.sync)

        }

    });

    // self.timeEnd('load'); 

}

/**
 * Prototype
 * 
 * @type  Object
 */
elFinder.prototype = {

    res: function (type, id) {
        return this.resources[type] && this.resources[type][id];
    },

    /**
	 * Internationalization object
	 * 
	 * @type  Object
	 */
    i18: {
        en: {
            translator: '',
            language: 'English',
            direction: 'ltr',
            dateFormat: 'd.m.Y H:i',
            fancyDateFormat: '$1 H:i',
            messages: {}
        },
        months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
        monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],

        days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
        daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    },

    /**
	 * File mimetype to kind mapping
	 * 
	 * @type  Object
	 */
    kinds: {
        'unknown': 'Unknown',
        'directory': 'Folder',
        'symlink': 'Alias',
        'symlink-broken': 'AliasBroken',
        'application/x-empty': 'TextPlain',
        'application/postscript': 'Postscript',
        'application/vnd.ms-office': 'MsOffice',
        'application/vnd.ms-word': 'MsWord',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'MsWord',
        'application/vnd.ms-word.document.macroEnabled.12': 'MsWord',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.template': 'MsWord',
        'application/vnd.ms-word.template.macroEnabled.12': 'MsWord',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'MsWord',
        'application/vnd.ms-excel': 'MsExcel',
        'application/vnd.ms-excel.sheet.macroEnabled.12': 'MsExcel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.template': 'MsExcel',
        'application/vnd.ms-excel.template.macroEnabled.12': 'MsExcel',
        'application/vnd.ms-excel.sheet.binary.macroEnabled.12': 'MsExcel',
        'application/vnd.ms-excel.addin.macroEnabled.12': 'MsExcel',
        'application/vnd.ms-powerpoint': 'MsPP',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'MsPP',
        'application/vnd.ms-powerpoint.presentation.macroEnabled.12': 'MsPP',
        'application/vnd.openxmlformats-officedocument.presentationml.slideshow': 'MsPP',
        'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': 'MsPP',
        'application/vnd.openxmlformats-officedocument.presentationml.template': 'MsPP',
        'application/vnd.ms-powerpoint.template.macroEnabled.12': 'MsPP',
        'application/vnd.ms-powerpoint.addin.macroEnabled.12': 'MsPP',
        'application/vnd.openxmlformats-officedocument.presentationml.slide': 'MsPP',
        'application/vnd.ms-powerpoint.slide.macroEnabled.12': 'MsPP',
        'application/pdf': 'PDF',
        'application/xml': 'XML',
        'application/vnd.oasis.opendocument.text': 'OO',
        'application/vnd.oasis.opendocument.text-template': 'OO',
        'application/vnd.oasis.opendocument.text-web': 'OO',
        'application/vnd.oasis.opendocument.text-master': 'OO',
        'application/vnd.oasis.opendocument.graphics': 'OO',
        'application/vnd.oasis.opendocument.graphics-template': 'OO',
        'application/vnd.oasis.opendocument.presentation': 'OO',
        'application/vnd.oasis.opendocument.presentation-template': 'OO',
        'application/vnd.oasis.opendocument.spreadsheet': 'OO',
        'application/vnd.oasis.opendocument.spreadsheet-template': 'OO',
        'application/vnd.oasis.opendocument.chart': 'OO',
        'application/vnd.oasis.opendocument.formula': 'OO',
        'application/vnd.oasis.opendocument.database': 'OO',
        'application/vnd.oasis.opendocument.image': 'OO',
        'application/vnd.openofficeorg.extension': 'OO',
        'application/x-shockwave-flash': 'AppFlash',
        'application/flash-video': 'Flash video',
        'application/x-bittorrent': 'Torrent',
        'application/javascript': 'JS',
        'application/rtf': 'RTF',
        'application/rtfd': 'RTF',
        'application/x-font-ttf': 'TTF',
        'application/x-font-otf': 'OTF',
        'application/x-rpm': 'RPM',
        'application/x-web-config': 'TextPlain',
        'application/xhtml+xml': 'HTML',
        'application/docbook+xml': 'DOCBOOK',
        'application/x-awk': 'AWK',
        'application/x-gzip': 'GZIP',
        'application/x-bzip2': 'BZIP',
        'application/zip': 'ZIP',
        'application/x-zip': 'ZIP',
        'application/x-rar': 'RAR',
        'application/x-tar': 'TAR',
        'application/x-7z-compressed': '7z',
        'application/x-jar': 'JAR',
        'text/plain': 'TextPlain',
        'text/x-php': 'PHP',
        'text/html': 'HTML',
        'text/javascript': 'JS',
        'text/css': 'CSS',
        'text/rtf': 'RTF',
        'text/rtfd': 'RTF',
        'text/x-c': 'C',
        'text/x-csrc': 'C',
        'text/x-chdr': 'CHeader',
        'text/x-c++': 'CPP',
        'text/x-c++src': 'CPP',
        'text/x-c++hdr': 'CPPHeader',
        'text/x-shellscript': 'Shell',
        'application/x-csh': 'Shell',
        'text/x-python': 'Python',
        'text/x-java': 'Java',
        'text/x-java-source': 'Java',
        'text/x-ruby': 'Ruby',
        'text/x-perl': 'Perl',
        'text/x-sql': 'SQL',
        'text/xml': 'XML',
        'text/x-comma-separated-values': 'CSV',
        'image/x-ms-bmp': 'BMP',
        'image/jpeg': 'JPEG',
        'image/gif': 'GIF',
        'image/png': 'PNG',
        'image/tiff': 'TIFF',
        'image/x-targa': 'TGA',
        'image/vnd.adobe.photoshop': 'PSD',
        'image/xbm': 'XBITMAP',
        'image/pxm': 'PXM',
        'audio/mpeg': 'AudioMPEG',
        'audio/midi': 'AudioMIDI',
        'audio/ogg': 'AudioOGG',
        'audio/mp4': 'AudioMPEG4',
        'audio/x-m4a': 'AudioMPEG4',
        'audio/wav': 'AudioWAV',
        'audio/x-mp3-playlist': 'AudioPlaylist',
        'video/x-dv': 'VideoDV',
        'video/mp4': 'VideoMPEG4',
        'video/mpeg': 'VideoMPEG',
        'video/x-msvideo': 'VideoAVI',
        'video/quicktime': 'VideoMOV',
        'video/x-ms-wmv': 'VideoWM',
        'video/x-flv': 'VideoFlash',
        'video/x-matroska': 'VideoMKV',
        'video/ogg': 'VideoOGG'
    },

    /**
	 * Ajax request data validation rules
	 * 
	 * @type  Object
	 */
    rules: {
        defaults: function (data) {
            if (!data
			|| (data.added && !$.isArray(data.added))
			|| (data.removed && !$.isArray(data.removed))
			|| (data.changed && !$.isArray(data.changed))) {
                return false;
            }
            return true;
        },
        open: function (data) { return data && data.cwd && data.files && $.isPlainObject(data.cwd) && $.isArray(data.files); },
        tree: function (data) { return data && data.tree && $.isArray(data.tree); },
        parents: function (data) { return data && data.tree && $.isArray(data.tree); },
        tmb: function (data) { return data && data.images && ($.isPlainObject(data.images) || $.isArray(data.images)); },
        upload: function (data) { return data && ($.isPlainObject(data.added) || $.isArray(data.added)); },
        search: function (data) { return data && data.files && $.isArray(data.files) }
    },




    /**
	 * Commands costructors
	 *
	 * @type Object
	 */
    commands: {},

    parseUploadData: function (text) {
        var data;

        if (!$.trim(text)) {
            return { error: ['errResponse', 'errDataEmpty'] };
        }

        try {
            data = $.parseJSON(text);
        } catch (e) {
            return { error: ['errResponse', 'errDataNotJSON'] }
        }

        if (!this.validResponse('upload', data)) {
            return { error: ['errResponse'] };
        }
        data = this.normalize(data);
        data.removed = $.map(data.added || [], function (f) { return f.hash; })
        return data;

    },

    iframeCnt: 0,

    uploads: {
        // upload transport using iframe
        iframe: function (data, fm) {
            var self = fm ? fm : this,
				input = data.input,
				dfrd = $.Deferred()
					.fail(function (error) {
					    error && self.error(error);
					})
					.done(function (data) {
					    data.warning && self.error(data.warning);
					    data.removed && self.remove(data);
					    data.added && self.add(data);
					    data.changed && self.change(data);
					    self.trigger('upload', data);
					    data.sync && self.sync();
					}),
				name = 'iframe-' + self.namespace + (++self.iframeCnt),
				form = $('<form action="' + self.uploadURL + '" method="post" enctype="multipart/form-data" encoding="multipart/form-data" target="' + name + '" style="display:none"><input type="hidden" name="cmd" value="upload" /></form>'),
				msie = this.UA.IE,
				// clear timeouts, close notification dialog, remove form/iframe
				onload = function () {
				    abortto && clearTimeout(abortto);
				    notifyto && clearTimeout(notifyto);
				    notify && self.notify({ type: 'upload', cnt: -cnt });

				    setTimeout(function () {
				        msie && $('<iframe src="javascript:false;"/>').appendTo(form);
				        form.remove();
				        iframe.remove();
				    }, 100);
				},
				iframe = $('<iframe src="' + (msie ? 'javascript:false;' : 'about:blank') + '" name="' + name + '" style="position:absolute;left:-1000px;top:-1000px" />')
					.bind('load', function () {
					    iframe.unbind('load')
							.bind('load', function () {
							    var data = self.parseUploadData(iframe.contents().text());

							    onload();
							    data.error ? dfrd.reject(data.error) : dfrd.resolve(data);
							});

					    // notify dialog
					    notifyto = setTimeout(function () {
					        notify = true;
					        self.notify({ type: 'upload', cnt: cnt });
					    }, self.options.notifyDelay);

					    // emulate abort on timeout
					    if (self.options.iframeTimeout > 0) {
					        abortto = setTimeout(function () {
					            onload();
					            dfrd.reject([errors.connect, errors.timeout]);
					        }, self.options.iframeTimeout);
					    }

					    form.submit();
					}),
				cnt, notify, notifyto, abortto

            ;

            if (input && $(input).is(':file') && $(input).val()) {
                form.append(input);
            } else {
                return dfrd.reject();
            }

            cnt = input.files ? input.files.length : 1;

            form.append('<input type="hidden" name="' + (self.newAPI ? 'target' : 'current') + '" value="' + self.cwd().hash + '"/>')
				.append('<input type="hidden" name="html" value="1"/>')
				.append($(input).attr('name', 'upload[]'));

            $.each(self.options.onlyMimes || [], function (i, mime) {
                form.append('<input type="hidden" name="mimes[]" value="' + mime + '"/>');
            });

            $.each(self.options.customData, function (key, val) {
                form.append('<input type="hidden" name="' + key + '" value="' + val + '"/>');
            });

            form.appendTo('body');
            iframe.appendTo('body');

            return dfrd;
        },
        // upload transport using XMLHttpRequest
        xhr: function (data, fm) {
            var self = fm ? fm : this,
				dfrd = $.Deferred()
					.fail(function (error) {
					    error && self.error(error);
					})
					.done(function (data) {
					    data.warning && self.error(data.warning);
					    data.removed && self.remove(data);
					    data.added && self.add(data);
					    data.changed && self.change(data);
					    self.trigger('upload', data);
					    data.sync && self.sync();
					})
					.always(function () {
					    notifyto && clearTimeout(notifyto);
					    notify && self.notify({ type: 'upload', cnt: -cnt, progress: 100 * cnt });
					}),
				xhr = new XMLHttpRequest(),
				formData = new FormData(),
				files = data.input ? data.input.files : data.files,
				cnt = files.length,
				loaded = 5,
				notify = false,
				startNotify = function () {
				    return setTimeout(function () {
				        notify = true;
				        self.notify({ type: 'upload', cnt: cnt, progress: loaded * cnt });
				    }, self.options.notifyDelay);
				},
				notifyto;

            if (!cnt) {
                return dfrd.reject();
            }

            xhr.addEventListener('error', function () {
                dfrd.reject('errConnect');
            }, false);

            xhr.addEventListener('abort', function () {
                dfrd.reject(['errConnect', 'errAbort']);
            }, false);

            xhr.addEventListener('load', function () {
                var status = xhr.status, data;

                if (status > 500) {
                    return dfrd.reject('errResponse');
                }
                if (status != 200) {
                    return dfrd.reject('errConnect');
                }
                if (xhr.readyState != 4) {
                    return dfrd.reject(['errConnect', 'errTimeout']); // am i right?
                }
                if (!xhr.responseText) {
                    return dfrd.reject(['errResponse', 'errDataEmpty']);
                }

                data = self.parseUploadData(xhr.responseText);
                data.error ? dfrd.reject(data.error) : dfrd.resolve(data);
            }, false);

            xhr.upload.addEventListener('progress', function (e) {
                var prev = loaded, curr;

                if (e.lengthComputable) {

                    curr = parseInt(e.loaded * 100 / e.total);

                    // to avoid strange bug in safari (not in chrome) with drag&drop.
                    // bug: macos finder opened in any folder,
                    // reset safari cache (option+command+e), reload elfinder page,
                    // drop file from finder
                    // on first attempt request starts (progress callback called ones) but never ends.
                    // any next drop - successfull.
                    if (curr > 0 && !notifyto) {
                        notifyto = startNotify();
                    }

                    if (curr - prev > 4) {
                        loaded = curr;
                        notify && self.notify({ type: 'upload', cnt: 0, progress: (loaded - prev) * cnt });
                    }
                }
            }, false);


            xhr.open('POST', self.uploadURL, true);
            formData.append('cmd', 'upload');
            formData.append(self.newAPI ? 'target' : 'current', self.cwd().hash);
            $.each(self.options.customData, function (key, val) {
                formData.append(key, val);
            });
            $.each(self.options.onlyMimes, function (i, mime) {
                formData.append('mimes[' + i + ']', mime);
            });

            $.each(files, function (i, file) {
                formData.append('upload[]', file);
            });

            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 0) {
                    // ff bug while send zero sized file
                    // for safari - send directory
                    dfrd.reject(['errConnect', 'errAbort']);
                }
            }

            xhr.send(formData);

            if (!this.UA.Safari || !data.files) {
                notifyto = startNotify();
            }

            return dfrd;
        }
    },


    /**
	 * Bind callback to event(s) The callback is executed at most once per event.
	 * To bind to multiply events at once, separate events names by space
	 *
	 * @param  String    event name
	 * @param  Function  callback
	 * @return elFinder
	 */
    one: function (event, callback) {
        var self = this,
			h = $.proxy(callback, function (event) {
			    setTimeout(function () { self.unbind(event.type, h); }, 3);
			    return callback.apply(this, arguments);
			});
        return this.bind(event, h);
    },

    /**
	 * Set/get data into/from localStorage
	 *
	 * @param  String       key
	 * @param  String|void  value
	 * @return String
	 */
    localStorage: function (key, val) {
        var s = window.localStorage;

        key = 'elfinder-' + key + this.id;

        if (val === null) {
            console.log('remove', key)
            return s.removeItem(key);
        }

        if (val !== void (0)) {
            try {
                s.setItem(key, val);
            } catch (e) {
                s.clear();
                s.setItem(key, val);
            }
        }

        return s.getItem(key);
    },

    /**
	 * Get/set cookie
	 *
	 * @param  String       cookie name
	 * @param  String|void  cookie value
	 * @return String
	 */
    cookie: function (name, value) {
        var d, o, c, i;

        name = 'elfinder-' + name + this.id;

        if (value === void (0)) {
            if (document.cookie && document.cookie != '') {
                c = document.cookie.split(';');
                name += '=';
                for (i = 0; i < c.length; i++) {
                    c[i] = $.trim(c[i]);
                    if (c[i].substring(0, name.length) == name) {
                        return decodeURIComponent(c[i].substring(name.length));
                    }
                }
            }
            return '';
        }

        o = $.extend({}, this.options.cookie);
        if (value === null) {
            value = '';
            o.expires = -1;
        }
        if (typeof (o.expires) == 'number') {
            d = new Date();
            d.setTime(d.getTime() + (o.expires * 86400000));
            o.expires = d;
        }
        document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + o.expires.toUTCString() + (o.path ? '; path=' + o.path : '') + (o.domain ? '; domain=' + o.domain : '') + (o.secure ? '; secure' : '');
        return value;
    },

    /**
	 * Get/set last opened directory
	 * 
	 * @param  String|undefined  dir hash
	 * @return String
	 */
    lastDir: function (hash) {
        return this.options.rememberLastDir ? this.storage('lastdir', hash) : '';
    },

    /**
	 * Node for escape html entities in texts
	 * 
	 * @type jQuery
	 */
    _node: $('<span/>'),

    /**
	 * Replace not html-safe symbols to html entities
	 * 
	 * @param  String  text to escape
	 * @return String
	 */
    escape: function (name) {
        return this._node.text(name).html();
    },

    /**
	 * Cleanup ajax data.
	 * For old api convert data into new api format
	 * 
	 * @param  String  command name
	 * @param  Object  data from backend
	 * @return Object
	 */
    normalize: function (data) {
        var filter = function (file) {

            if (file && file.hash && file.name && file.mime) {
                if (file.mime == 'application/x-empty') {
                    file.mime = 'text/plain';
                }
                return file;
            }
            return null;
            return file && file.hash && file.name && file.mime ? file : null;
        };


        if (data.files) {
            data.files = $.map(data.files, filter);
        }
        if (data.tree) {
            data.tree = $.map(data.tree, filter);
        }
        if (data.added) {
            data.added = $.map(data.added, filter);
        }
        if (data.changed) {
            data.changed = $.map(data.changed, filter);
        }
        if (data.api) {
            data.init = true;
        }
        return data;
    },

    /**
	 * Update sort options
	 *
	 * @param {String} sort type
	 * @param {String} sort order
	 * @param {Boolean} show folder first
	 */
    setSort: function (type, order, stickFolders) {
        this.storage('sortType', (this.sortType = this.sortRules[type] ? type : 'name'));
        this.storage('sortOrder', (this.sortOrder = /asc|desc/.test(order) ? order : 'asc'));
        this.storage('sortStickFolders', (this.sortStickFolders = !!stickFolders) ? 1 : '');
        this.trigger('sortchange');
    },

    _sortRules: {
        name: function (file1, file2) { return file1.name.toLowerCase().localeCompare(file2.name.toLowerCase()); },
        size: function (file1, file2) {
            var size1 = parseInt(file1.size) || 0,
				size2 = parseInt(file2.size) || 0;

            return size1 == size2 ? 0 : size1 > size2 ? 1 : -1;
            return (parseInt(file1.size) || 0) > (parseInt(file2.size) || 0) ? 1 : -1;
        },
        kind: function (file1, file2) { return file1.mime.localeCompare(file2.mime); },
        date: function (file1, file2) {
            var date1 = file1.ts || file1.date,
				date2 = file2.ts || file2.date;

            return date1 == date2 ? 0 : date1 > date2 ? 1 : -1
        }
    },

    /**
	 * Compare files based on elFinder.sort
	 *
	 * @param  Object  file
	 * @param  Object  file
	 * @return Number
	 */
    compare: function (file1, file2) {
        var self = this,
			type = self.sortType,
			asc = self.sortOrder == 'asc',
			stick = self.sortStickFolders,
			rules = self.sortRules,
			sort = rules[type],
			d1 = file1.mime == 'directory',
			d2 = file2.mime == 'directory',
			res;

        if (stick) {
            if (d1 && !d2) {
                return -1;
            } else if (!d1 && d2) {
                return 1;
            }
        }

        res = asc ? sort(file1, file2) : sort(file2, file1);

        return type != 'name' && res == 0
			? res = asc ? rules.name(file1, file2) : rules.name(file2, file1)
			: res;
    },

    /**
	 * Sort files based on config
	 *
	 * @param  Array  files
	 * @return Array
	 */
    sortFiles: function (files) {
        return files.sort(this.compare);
    },

    /**
	 * Open notification dialog 
	 * and append/update message for required notification type.
	 *
	 * @param  Object  options
	 * @example  
	 * this.notify({
	 *    type : 'copy',
	 *    msg : 'Copy files', // not required for known types @see this.notifyType
	 *    cnt : 3,
	 *    hideCnt : false, // true for not show count
	 *    progress : 10 // progress bar percents (use cnt : 0 to update progress bar)
	 * })
	 * @return elFinder
	 */
    notify: function (opts) {
        var type = opts.type,
			msg = this.messages['ntf' + type] ? this.i18n('ntf' + type) : this.i18n('ntfsmth'),
			ndialog = this.ui.notify,
			notify = ndialog.children('.elfinder-notify-' + type),
			ntpl = '<div class="elfinder-notify elfinder-notify-{type}"><span class="elfinder-dialog-icon elfinder-dialog-icon-{type}"/><span class="elfinder-notify-msg">{msg}</span> <span class="elfinder-notify-cnt"/><div class="elfinder-notify-progressbar"><div class="elfinder-notify-progress"/></div></div>',
			delta = opts.cnt,
			progress = opts.progress >= 0 && opts.progress <= 100 ? opts.progress : 0,
			cnt, total, prc;

        if (!type) {
            return this;
        }

        if (!notify.length) {
            notify = $(ntpl.replace(/\{type\}/g, type).replace(/\{msg\}/g, msg))
				.appendTo(ndialog)
				.data('cnt', 0);

            if (progress) {
                notify.data({ progress: 0, total: 0 });
            }
        }

        cnt = delta + parseInt(notify.data('cnt'));

        if (cnt > 0) {
            !opts.hideCnt && notify.children('.elfinder-notify-cnt').text('(' + cnt + ')');
            ndialog.is(':hidden') && ndialog.elfinderdialog('open');
            notify.data('cnt', cnt);

            if (progress < 100
			&& (total = notify.data('total')) >= 0
			&& (prc = notify.data('progress')) >= 0) {

                total = delta + parseInt(notify.data('total'));
                prc = progress + prc;
                progress = parseInt(prc / total);
                notify.data({ progress: prc, total: total });

                ndialog.find('.elfinder-notify-progress')
					.animate({
					    width: (progress < 100 ? progress : 100) + '%'
					}, 20);
            }

        } else {
            notify.remove();
            !ndialog.children().length && ndialog.elfinderdialog('close');
        }

        return this;
    },

    /**
	 * Open confirmation dialog 
	 *
	 * @param  Object  options
	 * @example  
	 * this.confirm({
	 *    title : 'Remove files',
	 *    text  : 'Here is question text',
	 *    accept : {  // accept callback - required
	 *      label : 'Continue',
	 *      callback : function(applyToAll) { fm.log('Ok') }
	 *    },
	 *    cancel : { // cancel callback - required
	 *      label : 'Cancel',
	 *      callback : function() { fm.log('Cancel')}
	 *    },
	 *    reject : { // reject callback - optionally
	 *      label : 'No',
	 *      callback : function(applyToAll) { fm.log('No')}
	 *   },
	 *   all : true  // display checkbox "Apply to all"
	 * })
	 * @return elFinder
	 */
    confirm: function (opts) {
        var complete = false,
			options = {
			    cssClass: 'elfinder-dialog-confirm',
			    modal: true,
			    resizable: false,
			    title: this.i18n(opts.title || 'confirmReq'),
			    buttons: {},
			    close: function () {
			        !complete && opts.cancel.callback();
			        $(this).elfinderdialog('destroy');
			    }
			},
			apply = this.i18n('apllyAll'),
			label, checkbox;


        if (opts.reject) {
            options.buttons[this.i18n(opts.reject.label)] = function () {
                opts.reject.callback(!!(checkbox && checkbox.prop('checked')))
                complete = true;
                $(this).elfinderdialog('close')
            };
        }

        options.buttons[this.i18n(opts.accept.label)] = function () {
            opts.accept.callback(!!(checkbox && checkbox.prop('checked')))
            complete = true;
            $(this).elfinderdialog('close')
        };

        options.buttons[this.i18n(opts.cancel.label)] = function () {
            $(this).elfinderdialog('close')
        };

        if (opts.all) {
            if (opts.reject) {
                options.width = 370;
            }
            options.create = function () {
                checkbox = $('<input type="checkbox" />');
                $(this).next().children().before($('<label>' + apply + '</label>').prepend(checkbox));
            }

            options.open = function () {
                var pane = $(this).next(),
					width = parseInt(pane.children(':first').outerWidth() + pane.children(':last').outerWidth());

                if (width > parseInt(pane.width())) {
                    $(this).closest('.elfinder-dialog').width(width + 30);
                }
            }
        }

        return this.dialog('<span class="elfinder-dialog-icon elfinder-dialog-icon-confirm"/>' + this.i18n(opts.text), options);
    },

    /**
	 * Create unique file name in required dir
	 * 
	 * @param  String  file name
	 * @param  String  parent dir hash
	 * @return String
	 */
    uniqueName: function (prefix, phash) {
        var i = 0, ext = '', p, name;

        prefix = this.i18n(prefix);
        phash = phash || this.cwd().hash;

        if ((p = prefix.indexOf('.txt')) != -1) {
            ext = '.txt';
            prefix = prefix.substr(0, p);
        }

        name = prefix + ext;

        if (!this.fileByName(name, phash)) {
            return name;
        }
        while (i < 10000) {
            name = prefix + ' ' + (++i) + ext;
            if (!this.fileByName(name, phash)) {
                return name;
            }
        }
        return prefix + Math.random() + ext;
    },

    /**
	 * Return message translated onto current language
	 *
	 * @param  String|Array  message[s]
	 * @return String
	 **/
    i18n: function () {
        var self = this,
			messages = this.messages,
			input = [],
			ignore = [],
			message = function (m) {
			    var file;
			    if (m.indexOf('#') === 0) {
			        if ((file = self.file(m.substr(1)))) {
			            return file.name;
			        }
			    }
			    return m;
			},
			i, j, m;

        for (i = 0; i < arguments.length; i++) {
            m = arguments[i];

            if (typeof m == 'string') {
                input.push(message(m));
            } else if ($.isArray(m)) {
                for (j = 0; j < m.length; j++) {
                    if (typeof m[j] == 'string') {
                        input.push(message(m[j]));
                    }
                }
            }
        }

        for (i = 0; i < input.length; i++) {
            // dont translate placeholders
            if ($.inArray(i, ignore) !== -1) {
                continue;
            }
            m = input[i];
            // translate message
            m = messages[m] || m;
            // replace placeholders in message
            m = m.replace(/\$(\d+)/g, function (match, placeholder) {
                placeholder = i + parseInt(placeholder);
                if (placeholder > 0 && input[placeholder]) {
                    ignore.push(placeholder)
                }
                return input[placeholder] || '';
            });

            input[i] = m;
        }

        return $.map(input, function (m, i) { return $.inArray(i, ignore) === -1 ? m : null; }).join('<br>');
    },

    /**
	 * Convert mimetype into css classes
	 * 
	 * @param  String  file mimetype
	 * @return String
	 */
    mime2class: function (mime) {
        var prefix = 'elfinder-cwd-icon-';

        mime = mime.split('/');

        return prefix + mime[0] + (mime[0] != 'image' && mime[1] ? ' ' + prefix + mime[1].replace(/(\.|\+)/g, '-') : '');
    },

    /**
	 * Return localized kind of file
	 * 
	 * @param  Object|String  file or file mimetype
	 * @return String
	 */
    mime2kind: function (f) {
        var mime = typeof (f) == 'object' ? f.mime : f, kind;

        if (f.alias) {
            kind = 'Alias';
        } else if (this.kinds[mime]) {
            kind = this.kinds[mime];
        } else {
            if (mime.indexOf('text') === 0) {
                kind = 'Text';
            } else if (mime.indexOf('image') === 0) {
                kind = 'Image';
            } else if (mime.indexOf('audio') === 0) {
                kind = 'Audio';
            } else if (mime.indexOf('video') === 0) {
                kind = 'Video';
            } else if (mime.indexOf('application') === 0) {
                kind = 'App';
            } else {
                kind = mime;
            }
        }

        return this.messages['kind' + kind] ? this.i18n('kind' + kind) : mime;

        var mime = typeof (f) == 'object' ? f.mime : f,
			kind = this.kinds[mime] || 'unknown';

        if (f.alias) {
            kind = 'Alias';
        } else if (kind == 'unknown') {
            if (mime.indexOf('text') === 0) {
                kind = 'Text';
            } else if (mime.indexOf('image') === 0) {
                kind = 'Image';
            } else if (mime.indexOf('audio') === 0) {
                kind = 'Audio';
            } else if (mime.indexOf('video') === 0) {
                kind = 'Video';
            } else if (mime.indexOf('application') === 0) {
                kind = 'Application';
            }
        }

        return this.i18n(kind);
    },

    /**
	 * Return localized date
	 * 
	 * @param  Object  file object
	 * @return String
	 */
    formatDate: function (file, ts) {
        var self = this,
			ts = ts || file.ts,
			i18 = self.i18,
			date, format, output, d, dw, m, y, h, g, i, s;

        if (self.options.clientFormatDate && ts > 0) {

            date = new Date(ts * 1000);

            h = date[self.getHours]();
            g = h > 12 ? h - 12 : h;
            i = date[self.getMinutes]();
            s = date[self.getSeconds]();
            d = date[self.getDate]();
            dw = date[self.getDay]();
            m = date[self.getMonth]() + 1;
            y = date[self.getFullYear]();

            format = ts >= this.yesterday
				? this.fancyFormat
				: this.dateFormat;

            output = format.replace(/[a-z]/gi, function (val) {
                switch (val) {
                    case 'd': return d > 9 ? d : '0' + d;
                    case 'j': return d;
                    case 'D': return self.i18n(i18.daysShort[dw]);
                    case 'l': return self.i18n(i18.days[dw]);
                    case 'm': return m > 9 ? m : '0' + m;
                    case 'n': return m;
                    case 'M': return self.i18n(i18.monthsShort[m - 1]);
                    case 'F': return self.i18n(i18.months[m - 1]);
                    case 'Y': return y;
                    case 'y': return ('' + y).substr(2);
                    case 'H': return h > 9 ? h : '0' + h;
                    case 'G': return h;
                    case 'g': return g;
                    case 'h': return g > 9 ? g : '0' + g;
                    case 'a': return h > 12 ? 'pm' : 'am';
                    case 'A': return h > 12 ? 'PM' : 'AM';
                    case 'i': return i > 9 ? i : '0' + i;
                    case 's': return s > 9 ? s : '0' + s;
                }
                return val;
            });

            return ts >= this.yesterday
				? output.replace('$1', this.i18n(ts >= this.today ? 'Today' : 'Yesterday'))
				: output;
        } else if (file.date) {
            return file.date.replace(/([a-z]+)\s/i, function (a1, a2) { return self.i18n(a2) + ' '; });
        }

        return self.i18n('dateUnknown');
    },

    /**
	 * Return css class marks file permissions
	 * 
	 * @param  Object  file 
	 * @return String
	 */
    perms2class: function (o) {
        var c = '';

        if (!o.read && !o.write) {
            c = 'elfinder-na';
        } else if (!o.read) {
            c = 'elfinder-wo';
        } else if (!o.write) {
            c = 'elfinder-ro';
        }
        return c;
    },

    /**
	 * Return localized string with file permissions
	 * 
	 * @param  Object  file
	 * @return String
	 */
    formatPermissions: function (f) {
        var p = [];

        f.read && p.push(this.i18n('read'));
        f.write && p.push(this.i18n('write'));

        return p.length ? p.join(' ' + this.i18n('and') + ' ') : this.i18n('noaccess');
    },

    /**
	 * Return formated file size
	 * 
	 * @param  Number  file size
	 * @return String
	 */
    formatSize: function (s) {
        var n = 1, u = 'b';

        if (s == 'unknown') {
            return this.i18n('unknown');
        }

        if (s > 1073741824) {
            n = 1073741824;
            u = 'GB';
        } else if (s > 1048576) {
            n = 1048576;
            u = 'MB';
        } else if (s > 1024) {
            n = 1024;
            u = 'KB';
        }
        s = s / n;
        return (s > 0 ? n >= 1048576 ? s.toFixed(2) : Math.round(s) : 0) + ' ' + u;
    },


    navHash2Id: function (hash) {
        return 'nav-' + hash;
    },

    navId2Hash: function (id) {
        return typeof (id) == 'string' ? id.substr(4) : false;
    },

    log: function (m) { window.console && window.console.log && window.console.log(m); return this; },

    debug: function (type, m) {
        var d = this.options.debug;

        if (d == 'all' || d === true || ($.isArray(d) && $.inArray(type, d) != -1)) {
            window.console && window.console.log && window.console.log('elfinder debug: [' + type + '] [' + this.id + ']', m);
        }
        return this;
    },
    time: function (l) { window.console && window.console.time && window.console.time(l); },
    timeEnd: function (l) { window.console && window.console.timeEnd && window.console.timeEnd(l); }


}